feat(m2m): client_credentials grant at /oauth/token [Phase 1 — PR 5/5]#647
Merged
lakhansamani merged 2 commits intoJul 6, 2026
Conversation
Implements the RFC 6749 §4.4 client_credentials grant for service account (machine/workload) identities — the final PR of Workload Identity Phase 1. - Extend AuthTokenConfig with ServiceAccountID; CreateAccessToken branches to a machine token (sub=service account id, scope only, no roles/allowed_roles, no custom-user script). Human token path is unchanged. New CreateMachineAuthToken issues an access token only — no id_token, no refresh_token, no session token. - Machine tokens are stateful like all access tokens: registered in the memory store under a namespaced session key (service_account:<id>). login_method=service_account makes ValidateAccessToken derive the same key with zero changes to the validation path. - Token endpoint: dual-path client auth (Basic or form body), bcrypt secret verification, and a cost-12 dummy-hash timing mitigation so unknown vs inactive vs wrong-secret are timing/response indistinguishable. - Scope enforcement: requested scopes must be a subset of AllowedScopes (invalid_scope otherwise, no silent downgrade); omitted scope grants the full authorized set. Empty AllowedScopes is deny-all. - Audit + metrics on issuance, consistent with existing grants. Adds ParsedAllowedScopes() as the single source of truth for the stored scope string. Integration tests cover happy paths (Basic + form body), wrong/unknown/inactive client, scope over-request, omitted scope, and a full ValidateAccessToken round-trip.
Flagged in review so the global-ClientID audience choice reads as a scoped decision, not an oversight.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The final PR of Phase 1: the RFC 6749 §4.4
client_credentialsOAuth2 grant at/oauth/token. Everything else (storage, service layer, GraphQL, gRPC) is already merged.Chains on: #645 (merged into this integration branch)
What's in this PR
internal/token/auth_token.go—AuthTokenConfig.ServiceAccountID;CreateAccessTokenbranches to a new privatecreateMachineAccessTokenwhencfg.User == nil && cfg.ServiceAccountID != ""(human path is byte-for-byte unchanged); new publicCreateMachineAuthToken— access token only, no id_token, no refresh_token.internal/http_handlers/token.go— newScoperequest field,client_credentialsgrant branch reusing the existing Basic/form client-auth extraction, bcrypt (cost 12) secret verification, scope-subset enforcement, memory-store registration, audit + metrics.internal/storage/schemas/service_account.go—ParsedAllowedScopes()helper.AuthRecipeMethodServiceAccount,AuditActorTypeServiceAccount.token_client_credentials_test.go) plus a one-line fix to an existing compliance test that usedclient_credentialsas its "unsupported grant type" example (now usespassword, since client_credentials is supported).Key design decisions (flagging for review)
MemoryStoreProvider, not just JWT signature. Machine tokens uselogin_method: "service_account"so the existingValidateAccessTokenkey-derivation (login_method + ":" + sub) reconstructs the same session key the endpoint registers under — zero changes to the validator itself.AllowedScopesset (common client_credentials convention, not spec-mandated). Requested scopes must be a strict subset —invalid_scopeon any excess, no silent downgrade.invalid_clientbody — inactive-vs-wrong-secret and unknown-vs-wrong-secret are indistinguishable by timing or response shape.IsActive=falseblocks new issuance, not already-issued tokens (until natural expiry) — matches the schema's documented behavior and the existing human-token revocation model.PR chain
Test plan
go build ./...,go vet ./...,make lint-go— 0 issuesmake test-sqlite— pass